package com.timebank.websocket.server;

import com.alibaba.fastjson.JSON;
import com.timebank.common.redis.utils.RedisUtil;
import com.timebank.websocket.consatnt.WebsocketConstant;
import com.timebank.websocket.pojo.dto.WebsocketMsgDTO;
import com.timebank.websocket.pojo.entity.OnlineUser;
import com.timebank.websocket.pojo.entity.RedisOnlineUser;
import com.timebank.websocket.pojo.vo.SendMsgVO;
import com.timebank.websocket.service.WebsocketService;
import com.timebank.websocket.topic.WebsocketMsgPublisher;
import com.timebank.websocket.utils.WebsocketSpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Websocket 监听类(连接，断点，消息发送等)
 * <p>
 * /websocket/{userId}/{username}   =  /websocket/用户Id/用户名 来连接websocket，该参数会带到每一个监听方法中
 * 同一个账号重复登录, 会被挤下线
 * </P>
 * @ServerEndpoint: socket链接地址
 *
 * @author shiliuyinzhen
 */
@ServerEndpoint(value = "/connect/{userId}/{userName}")
@Slf4j
@Component
public class WebsocketServer {


    /**
     * 所有用户信息(session + userId + username + createTime  --> 以用户的id为key, 通过用户key来获取用户session进行消息发送)
     */
    private final static Map<String, List<OnlineUser>> CLIENTS = new ConcurrentHashMap<>();

    /**
     * 获取在线用户列表
     *
     * @return {@link Map}<{@link String}, {@link List}<{@link OnlineUser}>>
     */
    public static Map<String, List<OnlineUser>> getClients() {
        return CLIENTS;
    }

    /**
     * 监听连接（有用户连接，立马到来执行这个方法），session 发生变化
     *
     * @param userId   用户id
     * @param userName 用户名
     * @param session  当前用户会话
     * @return void
     */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId, @PathParam("userName") String userName, Session session) {
        WebsocketService websocketService = WebsocketSpringContextUtil.getBean(WebsocketService.class);
        Integer onlineCount = websocketService.getOnlineCount();
        // 保存到当前服务器
        onlineLocal(userId, userName, session);
        onlineRedis(userId, userName, session, onlineCount);
        log.info("连接成功，userId：{},userName: {}" ,userId,userName);
        // 给自己发条消息,告诉自己连接成功
//        try {
//            SendMsgVO sendMsgVO = new SendMsgVO();
//            sendMsgVO.setSenderId(userId);
//            sendMsgVO.setSenderName(userName);
//            sendMsgVO.setGetterId(userId);
//            sendMsgVO.setContent(this.assembleNoticeMsg("系统消息", "欢迎您: " + userName));
//            sendMsgVO.setExtras(null);
//            sendMsgVO.setOnlineNum(websocketService.getOnlineCount());
//            session.getBasicRemote().sendText(JSON.toJSONString(sendMsgVO));
//        } catch (IOException e) {
//            throw new RuntimeException(e);
//        }
    }


    /**
     * 监听断开连接（有用户退出，会立马到来执行这个方法）
     *
     * @param userId   用户id
     * @param userName 用户名
     * @param session  当前用户会话
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId, @PathParam("userName") String userName, Session session) {

        String sessionId = session.getId();
        this.removeOnlineStatus(userId, session);
        // 关闭 (避免没有正常关闭主动在关闭一次)
        try {
            session.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        WebsocketService websocketService = WebsocketSpringContextUtil.getBean(WebsocketService.class);
        log.info("sessionId: " + sessionId + " -> " + userName + ":已离线！ 当前在线人数" + websocketService.getOnlineCount());
    }


    /**
     * 异常停止
     *
     * @param userId   用户id
     * @param userName 用户名
     * @param session  当前用户会话
     * @param e        异常信息
     */
    @OnError
    public void onError(@PathParam("userId") String userId, @PathParam("userName") String userName, Session session, Throwable e) {
        log.info("服务端发生了错误" + e);
        // 关闭 (避免没有正常关闭主动在关闭一次)
        try {
            session.close();
        } catch (IOException e1) {
            throw new RuntimeException(e1);
        }
    }


    /**
     * 监听消息发送（收到客户端的消息立即执行）
     *
     * @param userId   用户id
     * @param userName 用户名
     * @param message  传递的消息内容, json数据( to=接收人用户Id  (目标ID,逗号分隔) || content=内容  || content=消息类型)
     * @param session  当前用户会话
     *                 <p>
     *                 // 前端发送内容格式
     *                 ....
     *                 // 拼接参数
     *                 let message = {"content": "测试发送消息", "to": "ALL"}
     *                 ....
     */
    @OnMessage
    public void onMessage(@PathParam("userId") String userId, @PathParam("userName") String userName, String message, Session session) {
        WebsocketMsgPublisher websocketMsgPublisher = WebsocketSpringContextUtil.getBean(WebsocketMsgPublisher.class);
        WebsocketService websocketService = WebsocketSpringContextUtil.getBean(WebsocketService.class);
        // 请求参数（接收人+发送内容）
        try {
            WebsocketMsgDTO sendMsgDTO = JSON.parseObject(message, WebsocketMsgDTO.class);
            // 接收到消息 发送
            SendMsgVO sendMsgVO = new SendMsgVO();
            sendMsgVO.setSenderId(userId);
            sendMsgVO.setSenderName(userName);
            sendMsgVO.setGetterId(sendMsgDTO.getGetterId());
            sendMsgVO.setContent(sendMsgDTO.getContent());
            sendMsgVO.setExtras(null);
            sendMsgVO.setOnlineNum(websocketService.getOnlineCount());
            websocketMsgPublisher.sendMsg(sendMsgVO);
        } catch (Exception e) {
            // 给发送人 推送发送错误信息
            log.error("发送的消息格式错误");
            // 发送消息
            String content = "发送的消息格式错误";
            SendMsgVO sendMsgVO = new SendMsgVO();
            sendMsgVO.setSenderId(userId);
            sendMsgVO.setSenderName(userName);
            sendMsgVO.setGetterId(userId);
            sendMsgVO.setContent(content);
            sendMsgVO.setExtras(null);
            sendMsgVO.setOnlineNum(websocketService.getOnlineCount());
            websocketMsgPublisher.sendMsg(sendMsgVO);
        }
    }


    /**
     * 上线 保存在线状态到本地
     *
     * @param userId 用户id
     * @param userName 用户名
     * @param session 会话
     */
    private void onlineLocal(String userId, String userName, Session session) {
        if (CLIENTS.containsKey(userId)) {
            // 保存已存在用户的 session 会话
            CLIENTS.get(userId).add(new OnlineUser(userId, userName, session));
        } else {
            // 保存用户 session 会话
            ArrayList<OnlineUser> onlineUsers = new ArrayList<>();
            onlineUsers.add(new OnlineUser(userId, userName, session));
            CLIENTS.put(userId, onlineUsers);
        }
    }

    /**
     * 上线 保存在线状态 Redis
     *
     * @param userId 用户id
     * @param session 会话
     */
    private void onlineRedis(String userId, String userName, Session session, Integer onlineCount) {
        RedisUtil redisUtil = WebsocketSpringContextUtil.getBean(RedisUtil.class);
        // 保存到 redis
        boolean isUser = redisUtil.hHasKey(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId);
        if (isUser) {
            // 已存在, 多地登录追加重复登录用户信息到 redis 中
            List<RedisOnlineUser> redisOnlineUsers = (List<RedisOnlineUser>) redisUtil.hGet(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId);
            redisOnlineUsers.add(new RedisOnlineUser(userId, userName, session.getId()));
            redisUtil.hPut(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId, redisOnlineUsers);
            log.info("重复登录 ！sessionId：{} userId：{} -> {} userName：{} 当前在线人数:{}", session.getId(), redisOnlineUsers.size(), userId, userName, onlineCount);
        } else {
            // 保存用户到 redis 中
            ArrayList<RedisOnlineUser> redisOnlineUsers = new ArrayList<>();
            redisOnlineUsers.add(new RedisOnlineUser(userId, userName, session.getId()));
            redisUtil.hPut(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId, redisOnlineUsers);
            log.info("有新连接加入！sessionId：{} userId：{} userName：{} 当前在线人数:{}", session.getId(), userId, userName, onlineCount + 1);
        }
    }

    private void removeOnlineStatus(String userId, Session session) {
        synchronized (WebsocketServer.class) {
            // 移除本地在线状态
            this.offLineLocal(userId, session);
            // 移除 redis 在线状态
            this.offLineRedis(userId, session);
        }
    }


    /**
     * websocket 下线处理
     * session 连接状态
     */
    private void offLineLocal(String userId, Session session) {
        // 剔除下线用户 (当前服务中剔除)
        List<OnlineUser> onlineUsers = CLIENTS.get(userId);
        // 删除当前 session 连接
        for (OnlineUser onlineUser : onlineUsers) {
            if (onlineUser.getSession() == session) {
                onlineUsers.remove(onlineUser);
                break;
            }
        }
        // 如果同一用户已不存在任意连接
        if (onlineUsers.size() == 0) {
            CLIENTS.remove(userId);
        } else {
            CLIENTS.put(userId, onlineUsers);
        }
    }

    /**
     * 从 redis 中移除用户在线状态
     *
     * @param userId 用户id
     * @param session 会话
     */
    private void offLineRedis(String userId, Session session) {
        RedisUtil redisUtil = WebsocketSpringContextUtil.getBean(RedisUtil.class);
        List<RedisOnlineUser> redisOnlineUsers = (List<RedisOnlineUser>) redisUtil.hGet(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId);
        for (RedisOnlineUser redisOnlineUser : redisOnlineUsers) {
            if (redisOnlineUser.getSessionId().equals(session.getId())) {
                redisOnlineUsers.remove(redisOnlineUser);
                break;
            }
        }
        if (redisOnlineUsers.size() == 0) {
            redisUtil.hDelete(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId);
        } else {
            redisUtil.hPut(WebsocketConstant.WEBSOCKET_CACHE_KEY, userId, redisOnlineUsers);
        }
    }


    /**
     * 组装 消息通知需要的内容格式
     *
     * @param title  通知标题
     * @param message 通知消息
     */
    private String assembleNoticeMsg(String title, String message) {
        Map<String, String> noticeMsgData = new HashMap<>(2);
        Map<String, String> noticeContent = new HashMap<>(4);
        noticeContent.put("title", title);
        noticeContent.put("message", message);
        noticeMsgData.put("content", JSON.toJSONString(noticeContent));
        return JSON.toJSONString(noticeMsgData);
    }
}